//-----------------------------------------------------------------------
// Copyright (c) 2013 Terry Phillips
// Lava JS - http://lava.codeplex.com
// This license can be found here: http://lava.codeplex.com/license
//-----------------------------------------------------------------------
(function (window) {
    //'use strict';

    var Lava = {};
    Lava.isReady = false;
    Lava.dataBindName = 'data-bind';
    Lava.dataTemplateName = 'data-template';
    var attributeMaps = {};
    // default options for ajax requests
    var ajaxOptions = {
        type: 'POST',
        url: '/',
        contentType: 'application/json; charset=utf-8',
        data: {},
        success: function () { },
        error: function () { },
        async: true,
        timeout: 10000
    };

    Lava._bindings = {};

    var unSyncableTypes = ['function', 'computed'];
    var complexLavaProperties = ['bindings', 'computedProperties', 'clone', 'sync', 'extend'];

    var isSyncableType = function (type) {
        if (unSyncableTypes.indexOf(type) == -1) {
            return true;
        }
        return false;
    };

    var isComplexProperty = function (property) {
        if (complexLavaProperties.indexOf(property) > -1) {
            return true;
        }
        return false;
    };

    var showElement = function (element) {
        element.lava_shown = true;
        element.style.display = element.lava_display || '';
    };

    var hideElement = function (element) {
        element.lava_shown = false;
        element.lava_display = element.style.display;
        element.style.display = 'none';
    };

    Lava.ajaxSetup = function (options) {
        if (Lava.typeOf(options) === 'undefined') {
            return ajaxOptions;
        }
        Lava.extend(ajaxOptions, options, true);
    };

    // Determines the type of the object passed in
    Lava.typeOf = function (a) {
        if (typeof a === 'undefined') {
            return 'undefined';
        }
        if (a === null) {
            return 'null';
        }
        if (a.type === 'computed') {
            return 'computed';
        }
        if (Object.prototype.toString.call(a) === '[object Array]') {
            return 'array';
        }
        if (Object.prototype.toString.call(a) === '[object Object]') {
            return 'object';
        }
        if (Object.prototype.toString.call(a) === '[object Date]') {
            return 'date';
        }
        return typeof a;
    };

    var getFirstChild = function (el) {
        var firstChild = el.firstChild;
        while (firstChild != null && firstChild.nodeType == 3) { // skip TextNodes
            firstChild = firstChild.nextSibling;
        }
        return firstChild;
    }

    // Merges a source object into a target object
    // If overwrite is true, the operation will overwrite existing values
    Lava.extend = function (target, source, overwrite) {
        var _t = target || {};
        var _s = source || {};

        for (var name in _s) {
            if (_s.hasOwnProperty(name)) {
                if (complexLavaProperties.indexOf(name) > -1)
                    continue;

                var t = _t[name];
                var s = _s[name];

                if (_t === s) {
                    continue;
                }
                if (typeof t === 'undefined' || overwrite === true) {
                    if (Lava.typeOf(_s[name]) === 'array') {
                        _t[name] = _s[name].slice();
                    }
                    else {
                        _t[name] = _s[name];
                    }
                }
            }
        }
    };

    // Syncs all of the objects properties with the properties of the object passed in
    Lava.sync = function (target, source) {
        for (var property in target) {
            if (target.hasOwnProperty(property)) {
                var targetPropType = Lava.typeOf(target[property]);
                var sourcePropType = Lava.typeOf(source[property]);

                if (!isSyncableType(targetPropType) || !isSyncableType(sourcePropType)) {
                    continue;
                }
                if (isComplexProperty(property) || isComplexProperty(sourcePropType)) {
                    continue;
                }

                if (targetPropType === 'object' && sourcePropType === 'object') {
                    Lava.sync(target[property], source[property]);
                }
                else {
                    if (property[0] !== '_') {
                        // copy property value
                        target[property] = source[property];
                    }
                }
            }
        }
    };

    // Returns an object without Lava specific properties
    Lava.simplify = function (complex, simple) {
        var complex = complex || {};
        var simple = simple || {};

        for (var property in complex) {
            if (complex.hasOwnProperty(property)) {
                var complexPropType = Lava.typeOf(complex[property]);

                if (complexPropType == 'function' || isComplexProperty(property)) {
                    continue;
                }

                if (complexPropType === 'object') {
                    simple[property] = Lava.simplify(complex[property], simple[property]);
                }
                else {
                    if (property[0] !== '_') {
                        if (complexPropType == 'computed') {
                            simple[property] = complex[property].f.call(complex[property].context);
                        }
                        else {
                            if (Lava.typeOf(complex[property]) === 'date')
                                simple[property] = complex[property].toISOString();
                            else
                                simple[property] = complex[property];
                        }
                    }
                }
            }
        }
        return simple;
    };

    // Defers execution of a function until the DOM is ready
    Lava.ready = function (f) {
        if (Lava.isReady) {
            f();
        }
        else if (document.addEventListener) {
            document.addEventListener('DOMContentLoaded', function () {
                Lava.isReady = true;
                f();
            });
        }
        else {
            if (/in/.test(document.readyState)) {
                setTimeout(function () { Lava.ready(f); }, 9);
            }
            else {
                Lava.isReady = true;
                f.call(this);
            }
        }
        return this;
    };

    // Load data from the server using an HTTP GET request
    Lava.get = function (url, success, error) {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    if (success) success.call(this, xhr.responseText);
                }
                else if (error) {
                    options.error.call(context, xhr);
                }
            }
        };
        xhr.open('GET', url, true);
        xhr.timeout = 10000;
        xhr.setRequestHeader('Content-Type', 'text');
        xhr.send('{}');
    };

    // Performs an asynchronous HTTP request
    Lava.ajax = function (options, context) {
        var context = context || window;
        var settings = {};
        Lava.extend(settings, ajaxOptions, true);
        Lava.extend(settings, options, true);

        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var text = xhr.responseText.replace(/\\\/Date\((-?\d+)\)\\\//g, function (match, p1) {
                        return new Date(parseInt(p1)).toISOString();
                    });
                    settings.success.call(context, JSON.parse(text));
                }
                else {
                    settings.error.call(context, xhr);
                }
            }
        };
        xhr.open(settings.type, settings.url, settings.async);
        xhr.timeout = settings.timeout;
        xhr.setRequestHeader('Content-Type', settings.contentType);
        var data = Lava.simplify(settings.data);
        xhr.send(JSON.stringify(data));
    };

    // Converts an object string to JSON
    var convertObjectStrToJSON = function (obj) {
        var nonWritable = '"\'',
            strings = [],
            string = '',
            insideQuotes = false,
            oLen = obj.length,
            json = [];

        // replace brackets
        obj = obj.replace(/\[/g, '<').replace(/\]/g, '>');

        for (var a = 0; a < oLen; a++) {
            var c = obj[a];

            // handle strings inside quotes
            if (c === "'") {
                if (!insideQuotes) {
                    string = '';
                    insideQuotes = true;
                    json.push('[' + strings.length + ']');
                }
                else {
                    strings.push(string);
                    insideQuotes = false;
                }

                continue;
            }

            if (insideQuotes) {
                string += c;
                continue;
            }
            // END handle strings inside quotes

            // write the char to the output stram
            if (nonWritable.indexOf(c) === -1) {
                json.push(c);
            }
        }

        var jsonStr = json.join('');

        // wrap all keys and values with double quotes
        jsonStr = jsonStr.replace(/[^ \{\},:]+/g, function (a) {
            if (!isNaN(parseFloat(a)) && isFinite(a)) return a;
            return '"' + a + '"';
        });

        for (var a = 0; a < strings.length; a++) {
            jsonStr = jsonStr.replace('[' + a + ']', '"' + strings[a] + '"');
        }

        jsonStr = jsonStr.replace(/</g, '[').replace(/>/g, ']');


        // get rid of duplicate quotes
        jsonStr = jsonStr.replace(/\"\"/g, '"');

        return jsonStr;
    };

    // this method takes in an object expression (e.g. object.property) and resolves it
    var resolveObjectExpression = function (expression, context) {
        var objectName = 'window', object = context || window, property = null, propertyIsIndex = false;
        var objectFragments = expression.toString().split('.');

        for (var b = 0; b < objectFragments.length; b++) {
            property = objectFragments[b].trim();
            var tmp = null;
            var leftIdx = property.indexOf('['), rightIdx = property.indexOf(']');
            // handle special array syntax
            if (property.indexOf('[@each]') > -1) {
                objectName = property;
                object = object[property.replace('[@each]', '')];
                if (!object.computedProperties)
                    object.computedProperties = [];
            }
            else if (property.indexOf('[i]') == 0) {

            }
                // handle array
            else if (leftIdx > -1 && rightIdx > -1) {
                var array = property.substring(0, leftIdx);
                var index = property.substring(leftIdx + 1, rightIdx);
                tmp = object[array][index];
            }
            else {
                tmp = object[property];
            }

            if (Lava.typeOf(tmp) === 'object') {
                objectName = property;
                object = tmp;
            }
        }

        // check to see if property is unique.  If not, we do not actually have a property in the expression, just an object
        if (objectName === property) {
            property = null;
        }

        // check to see if the property is actually an array index
        if (property && property.indexOf('[') > -1 && property.slice(-1) == ']') {
            objectName = property.substring(0, property.indexOf('['));
            object = object[objectName];
            property = property.substring(property.indexOf('[') + 1, property.length - 1);
            propertyIsIndex = true;
        }

        return { objectName: objectName, object: object, property: property, propertyIsIndex: propertyIsIndex };
    };

    var resolveAttribute = function (attribute) {
        if (attributeMaps[attribute])
            return attributeMaps[attribute];
        else
            return attribute;
    };

    var getPathFromPropertyExpression = function (expression) {
        return expression.substring(0, expression.lastIndexOf('.'));
    };

    var replaceTemplateTags = function (text) {
        var regex = new RegExp(Lava.dataTemplateName, 'g');
        return text.replace(regex, Lava.dataBindName);
    };

    var resolveAllUnboundExpressions = function (text) {
        var left = text.indexOf('{{'), right = 0;
        var replaces = [];

        while (left > -1) {
            // get the closing brace
            right = text.indexOf('}}', left);

            var expression = text.substring(left + 2, right);
            var result = resolveObjectExpression(expression);

            // replace the expression with its value
            var regex = new RegExp('\\{\\{' + expression.replace(/\[/g, '\\[').replace(/\]/g, '\\]').replace(/\(/g, '\\(').replace(/\)/g, '\\)') + '\\}\\}', 'g');
            var value = result.object[result.property];
            if (Lava.typeOf(result.object[result.property]) === 'function') {
                value = result.object[result.property]();
            }
            else if (Lava.typeOf(result.object[result.property]) === 'computed') {
                value = result.object[result.property].get();
            }


            replaces.push({ regex: regex, value: value });

            // get new index value
            left = text.indexOf('{{', right);
            var i = 0;
        }

        for (var i = 0; i < replaces.length; i++) {
            var replace = replaces[i];
            text = text.replace(replace.regex, replace.value);
        }

        return text;
    }

    var closestLavaLoop = function (elem) {
        elem = elem.parentNode;

        while (elem) {
            if (elem.lava_loop) {
                return elem;
            } else {
                elem = elem.parentNode;
            }
        }
        return false;
    }

    var isElement = function (o) {
        return (typeof HTMLElement === "object" ? o instanceof HTMLElement : o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string");
    }

    var routes = {};
    var root = window.location.pathname;
    var routingEnabled = false;

    var cleanUpPath = function (route) {
        var routeLength = route.length;
        // remove trailing slash
        var route = (routeLength > 1 && route[routeLength - 1] === '/') ? route.substring(0, routeLength - 1) : route;
        // remove double slashes
        route = route.replace(/\/\//g, '');
        // remove trailing pound
        var route = (routeLength > 1 && route[routeLength - 1] === '#') ? route.substring(0, routeLength - 1) : route;
        return route;
    };

    Lava.Routes = {
        add: function (route, action) {
            if (!routingEnabled) {
                // set up the routing system
                window.onload = function (event) {
                    initialRoute = window.location.hash.replace('#', '');
                    Lava.Routes.go(initialRoute, true);
                };

                window.onpopstate = function (event) {
                    if (event.state) {
                        Lava.Routes.go(event.state, true);
                    }
                };

                window.onhashchange = function () {
                    Lava.Routes.go(window.location.hash.replace('#', ''), true);
                };

                routingEnabled = true;
            }

            routes[route] = { name: route, path: cleanUpPath(root + '#' + route), action: action };
        },
        get: function (route) {
            return routes[route];
        },
        getCurrent: function () {
            return routes[window.location.hash.replace('#', '')];
        },
        go: function (route, ignorePushState) {
            var routeObj = routes[route];
            if (typeof routeObj == 'undefined') {
                return false;
            }
            if (!ignorePushState) {
                window.history.pushState(routeObj.name, routeObj.name, routeObj.path);
            }
            if (routeObj.action) {
                routeObj.action();
            }
        }
    }

    // adds getter and setter methods to all properties on an object
    var addGetSet = function (lavaObject) {
        for (var property in lavaObject) {

            (function (n, obj) {
                var newKey = '_' + n;
                var objType = Lava.typeOf(obj[n]);
                if (obj.hasOwnProperty(n) && !Object.getOwnPropertyDescriptor(obj, n).get) {
                    if (objType === 'object') {
                        addGetSet(obj[n]);
                    }
                    else if (n != 'computedProperties') {
                        // add binding storage to the object
                        if (!obj.bindings) obj.bindings = {};
                        if (!obj.bindings[n]) obj.bindings[n] = [];

                        if (objType === 'array') {
                            // add array specific binding storage
                            if (!obj[n].bindings) obj[n].bindings = {};
                            if (!obj[n].bindings.add) obj[n].bindings.add = [];
                            if (!obj[n].bindings.addAt) obj[n].bindings.addAt = [];
                            if (!obj[n].bindings.remove) obj[n].bindings.remove = [];
                            if (!obj[n].bindings.removeAt) obj[n].bindings.removeAt = [];
                        }

                        if (Lava.typeOf(obj[n]) === 'computed')
                            if (!obj[n].context)
                                obj[n].context = obj;

                        obj[newKey] = obj[n];
                        delete obj[n];
                        (function (object, key, name, type) {
                            Object.defineProperty(obj, name, {
                                get: function () {
                                    if (this.dependOn && this.dependOn.indexOf(name) === -1) {
                                        this.dependOn.push(name);
                                    }
                                    return this[key];
                                },
                                set: function (x) {
                                    this[key] = x;
                                    set(obj, name, obj[name]);
                                },
                                configurable: true,
                                enumerable: true
                            });
                            // set up array methods
                            if (type === 'array') {
                                obj[name].add = function (value) {
                                    obj[name].push(value);
                                    set(obj[name], 'add', value);
                                    return value;
                                };
                                obj[name].addAt = function (index, value) {
                                    obj[name].splice(index, 0, value);
                                    set(obj[name], 'addAt', { index: index, value: value });
                                    return value;
                                };
                                obj[name].remove = function (value) {
                                    set(obj[name], 'remove', value);
                                    obj[name].splice(obj[name].indexOf(value), 1);
                                    return value;
                                };
                                obj[name].removeAt = function (index) {
                                    var item = obj[name].splice(index, 1);
                                    set(obj[name], 'removeAt', index);
                                    return item.length > 0 ? item[0] : null;
                                };
                                obj[name].empty = function () {
                                    var len = obj[name].length;
                                    while (len--) {
                                        obj[name].removeAt(len);
                                    }
                                };
                                obj[name].fill = function (lavaArray) {
                                    obj[name].empty();
                                    var len = lavaArray.length;
                                    for (var i = 0; i < len; i++) {
                                        obj[name].add(lavaArray[i]);
                                    }
                                };
                                obj[name].where = function (f, callback) {
                                    var temp = [];
                                    for (var a = 0; a < obj[name].length; a++)
                                        if (f.call(obj[name], obj[name][a])) {
                                            temp.push(obj[name][a]);
                                            if (callback) {
                                                var currLen = obj[name].length;
                                                callback.call(obj[name][a]);
                                                var len = obj[name].length;
                                                if (currLen > len) a = a - (currLen - len);
                                            }
                                        }
                                    return temp;
                                };
                                obj[name].first = function (f, callback) {
                                    for (var a = 0; a < obj[name].length; a++)
                                        if (f.call(obj[name], obj[name][a])) {
                                            if (callback) {
                                                var currLen = obj[name].length;
                                                callback.call(obj[name][a]);
                                                var len = obj[name].length;
                                                if (currLen > len) a = a - (currLen - len);
                                            }
                                            return obj[name][a];
                                        }
                                };

                            }
                        }(this, newKey, n, objType));

                        if (!obj.computedProperties)
                            obj.computedProperties = [];
                    }
                }
            })(property, lavaObject);
        }
    };

    // sets the changed value of an object's property to all of its bound properties
    var set = function (object, property, value) {
        var propertyBindings = object.bindings[property];
        var len = propertyBindings.length;
        for (var a = 0; a < len; a++) {
            if (propertyBindings[a].func) {
                propertyBindings[a].func(value);
            }
            else {
                propertyBindings[a](value);
            }
        }
    };

    Lava.bind = function () {
        while (document.querySelectorAll('[' + Lava.dataBindName + ']').length > 0) {
            var elements = document.querySelectorAll('[' + Lava.dataBindName + ']');

            for (var a = 0; a < elements.length; a++) {
                (function (element) {

                    // get the binding expression
                    var expStr = element.getAttribute('' + Lava.dataBindName + '');
                    var expressions = JSON.parse(convertObjectStrToJSON(expStr));

                    for (var attribute in expressions) {
                        (function (attribute) {
                            if (expressions.hasOwnProperty(attribute)) {
                                var expression = expressions[attribute];

                                if (Lava.typeOf(expression) === 'object') {
                                    // this is a complex binding with multiple different properties in the binding expression
                                    // current properties that make up a complex binding: bind, mode, format, args
                                    // resolve the bind property
                                    var resolved = resolveObjectExpression(expression.bind);
                                    bindExpression(resolved.objectName, resolved.object, resolved.property, element, attribute, expression, resolved.propertyIsIndex);
                                }
                                else {
                                    var resolved = resolveObjectExpression(expression);
                                    bindExpression(resolved.objectName, resolved.object, resolved.property, element, attribute, expression, resolved.propertyIsIndex);
                                }
                            }
                        })(attribute);
                    }
                    element.removeAttribute(Lava.dataBindName);
                })(elements[a]);
            }
        }
    };

    var bindExpression = function (objectName, object, property, element, attribute, expression, propertyIsIndex) {
        var attribute = resolveAttribute(attribute);
        var func = null;

        // handle template attribute with static templateid
        if (attribute === 'template' && property == null) {
            element.innerHTML = replaceTemplateTags(document.getElementById(objectName).innerHTML);
            return;
        }

        // handle functions as properties that return a value
        var isNormalFunc = false;
        if (property.slice(-2) === '()') {
            property = property.substring(0, property.length - 2);
            isNormalFunc = true;
        }

        // choose the func based on the type of attribute we are dealing with
        if (Lava.typeOf(object[property]) === 'function') {
            if (isNormalFunc) {
                func = function (value) {
                    if (value) {
                        var v = resolveFormatExpressionProperty(value.call(object), expression);
                        if (element[attribute] != v)
                            element[attribute] = v;
                    }
                };
            }
            else {
                if (expression.args) {
                    // resolve argument expressions
                    var args = {};
                    for (var arg in expression.args) {
                        if (expression.args.hasOwnProperty(arg)) {
                            var tmp = resolveObjectExpression(expression.args[arg]);
                            if (tmp.property) {
                                if (tmp.object.hasOwnProperty(tmp.property)) {
                                    args[arg] = tmp.object[tmp.property];
                                }
                                else {
                                    args[arg] = tmp.property;
                                }
                            }
                            else {
                                // there is no property in the resolved expression so just set the arg to the object
                                args[arg] = tmp.object;
                            }
                        }
                    }

                    element.addEventListener(attribute, function (e) {
                        setTimeout(function () { object[property].call(object, e, args); }, 0);
                    }, false);
                }
                else {
                    element.addEventListener(attribute, function (e) {
                        setTimeout(function () { object[property].call(object, e); }, 0);
                    }, false);
                }
            }
        }
        else if (Lava.typeOf(object[property]) === 'computed') {
            // overwrite dependencies if array var is used
            //var path = getPathFromPropertyExpression(expression);
            //object[property].dependencies = object[property].dependencies.replace(/\[i\]/g, path);
            bindComputedProperty(object, property, element, attribute, expression);
        }
        else if (attribute.indexOf('style.') === 0) {
            var subAttribute = attribute.replace('style.', '');

            func = function (value) {
                value = resolveFormatExpressionProperty(value, expression);
                if (element.style[subAttribute] != value)
                    element.style[subAttribute] = value;
            };
        }
        else if (attribute === 'foreach') {
            var array = object[property];

            if (Lava.typeOf(array) === 'array') {
                var tagName = element.tagName.toLowerCase();
                // we need to wrap the children in a parent element if there is more than one child
                if (element.children.length > 1) {
                    if (tagName == 'ul') {
                        element.innerHTML = '<li>' + element.innerHTML + '</li>';
                    }
                    else if (tagName == 'table') {
                        element.innerHTML = '<tr>' + element.innerHTML + '</tr>';
                    }
                    else if (tagName == 'tr') {
                        element.innerHTML = '<td>' + element.innerHTML + '</td>';
                    }
                    else {
                        element.innerHTML = '<div>' + element.innerHTML + '</div>';
                    }
                }

                var template = element.innerHTML;

                if (expression.template) {
                    // get the template
                    var templateElement = document.getElementById(expression.template);
                    // create the correct type of tempElement.  This is necessary because if we wrap, for example,
                    // tr elements with a div to test, the tr will be ignored.
                    var tempElement = document.createElement(tagName);
                    tempElement.innerHTML = templateElement.innerHTML;
                    if (tempElement.children.length > 1) {
                        if (tagName == 'ul') {
                            templateElement.innerHTML = '<li>' + templateElement.innerHTML + '</li>';
                        }
                        else if (tagName === 'table') {
                            template = '<tr>' + templateElement.innerHTML + '</tr>';
                        }
                        else if (tagName === 'tbody') {
                            template = '<tr>' + templateElement.innerHTML + '</tr>';
                        }
                        else if (tagName === 'thead') {
                            template = '<tr>' + templateElement.innerHTML + '</tr>';
                        }
                        else if (tagName === 'tr') {
                            template = '<td>' + templateElement.innerHTML + '</td>';
                        }
                        else {
                            templateElement.innerHTML = '<div>' + templateElement.innerHTML + '</div>';
                        }
                    }

                    template = templateElement.innerHTML;
                    // get rid of the temporary element
                    delete tempElement;
                }

                element.innerHTML = '';
                var html = '';

                // get previous lava_loop name if exists
                var lava_loop = '';
                var closest = closestLavaLoop(element);
                if (closest) {
                    lava_loop = closest.lava_loop + '.';
                }

                for (var a = 0; a < array.length; a++) {
                    html += replaceTemplateTags(template.replace(/\[i\]/g, lava_loop + objectName + '.' + property + '[' + a + ']')).replace(/\{i\}/g, a);
                }

                element.lava_loop = lava_loop + objectName;
                html = resolveAllUnboundExpressions(html);
                addTemplate(tagName, html, element, -1, false);

                // function to handle adding item to array
                var add = function (value) {
                    var index = array.length - 1;
                    var html = replaceTemplateTags(template.replace(/\[i\]/g, lava_loop + objectName + '.' + property + '[' + index + ']')).replace(/\{i\}/g, a);
                    html = resolveAllUnboundExpressions(html);

                    addTemplate(tagName, html, element, -1, true);

                    Lava.bind();
                    // take care of computed properties
                    var computedProperties = array.computedProperties || [];
                    for (var a = 0; a < computedProperties.length; a++) {
                        rebindComputedProperty(computedProperties[a], element);
                    }
                };
                addBinding(array.bindings.add, add, attribute, element);
                // function to handle adding item to array at index
                var addAt = function (value) {
                    var html = replaceTemplateTags(template.replace(/\[i\]/g, lava_loop + objectName + '.' + property + '[' + value.index + ']')).replace(/\{i\}/g, a);
                    html = resolveAllUnboundExpressions(html);
                    var d = document.createElement('div');
                    d.innerHTML = html;
                    var newNode = d.firstElementChild;
                    if (element.children.length === 0)
                        addTemplate(tagName, html, element, -1, false);
                    else
                        addTemplate(tagName, html, element, value.index, false, newNode);

                    d = null;
                    Lava.bind();
                    // take care of computed properties
                    var computedProperties = array.computedProperties || [];
                    for (var a = 0; a < computedProperties.length; a++) {
                        rebindComputedProperty(computedProperties[a], element);
                    }
                };
                addBinding(array.bindings.addAt, addAt, attribute, element);
                // function to handle removing item from array
                var remove = function (value) {
                    var index = array.indexOf(value);
                    if (index > -1) {
                        element.removeChild(element.children[index]);
                    }
                };
                addBinding(array.bindings.remove, remove, attribute, element);
                // function to handle removing item from array at index
                var removeAt = function (value) {
                    if (value > -1) {
                        element.removeChild(element.children[value]);
                    }
                };
                addBinding(array.bindings.removeAt, removeAt, attribute, element);
            }
        }
        else if (attribute === 'if') {
            func = function (value) {
                var shown = element.lava_shown;
                if (Lava.typeOf(shown) === 'undefined') {
                    // the if block has not been processed yet
                    if (value)
                        element.lava_shown = true;
                    else
                        hideElement(element);
                }
                else if (shown === true && value === false) {
                    hideElement(element);
                }
                else if (shown === false && value === true) {
                    showElement(element);
                }
            }
        }
        else if (attribute === 'ifnot') {
            func = function (value) {
                value = !value;
                var shown = element.lava_shown;
                if (Lava.typeOf(shown) === 'undefined') {
                    // the if block has not been processed yet
                    if (value)
                        element.lava_shown = true;
                    else
                        hideElement(element);
                }
                else if (shown === true && value === false) {
                    hideElement(element);
                }
                else if (shown === false && value === true) {
                    showElement(element);
                }
            }
        }
        else if (attribute === 'class') {

        }
            // handle template attribute with property
        else if (attribute === 'template') {
            func = function (value) {
                if (isElement(value)) {
                    element.innerHTML = replaceTemplateTags(value.innerHTML);
                }
                else {
                    element.innerHTML = replaceTemplateTags(document.getElementById(value).innerHTML);
                }
                setTimeout(function () { Lava.bind(); }, 0);
            }
        }
        else {
            func = function (value) {
                value = resolveFormatExpressionProperty(value, expression);
                if (element[attribute] !== value)
                    element[attribute] = value;
            };
        }

        // set up two-way binding for value attribute
        if (attribute === 'value' && expression.mode != 'oneWay') {
            var elementType = element.tagName.toLowerCase()
            var typeAttr = element.getAttribute('type');
            if (typeAttr)
                elementType += ':' + typeAttr.toLowerCase();
            if ('input:checkbox input:radio select'.indexOf(elementType) > -1) {
                element.addEventListener('change', function () {
                    setTimeout(function () { object[property] = element.value; }, 0);
                }, false);
            }
            else if ('input:text input:password textarea'.indexOf(elementType) > -1) {
                element.addEventListener('keyup', function () {
                    setTimeout(function () { object[property] = element.value; }, 0);
                }, false);
                element.addEventListener('keydown', function () {
                    setTimeout(function () { object[property] = element.value; }, 0);
                }, false);
            }
        }

        // set up two-way binding for checked
        if (attribute === 'checked' && expression.mode != 'oneWay') {
            var elementType = element.tagName.toLowerCase() + ':' + element.getAttribute('type').toLowerCase();
            if ('input:checkbox input:radio'.indexOf(elementType) > -1) {
                element.addEventListener('change', function () {
                    /*setTimeout(function () { */object[property] = element.checked; /*}, 0);*/
                }, false);
            }
        }

        // add the default binding function if present.  This will be null for array type functions
        if (func) {
            if (object.bindings) {
                // only add binding if it does not already exist - REMOVED - this was not allowing more than one of the same type attribute to
                // be bound to a property
                var found = false;
                /*for (var i = 0; i < object.bindings[property].length; i++) {
                    if (object.bindings[property][i].attribute === attribute) {
                        found = true;
                        break;
                    }
                }*/
                if (!found)
                    object.bindings[property].push({ func: func, attribute: attribute });
            }
            func(object[property]);
        }
    };

    var bindComputedProperty = function (object, property, element, attribute, expression) {
        var computed = object[property],
            context = computed.context,
            arr = [];
        if (computed.dependencies) {
            arr = computed.dependencies.split(',');
        } else {
            context.dependOn = arr;
            computed.f.call(context);
            delete context.dependOn;
        }
        computed.properties = [];
        for (var a = 0; a < arr.length; a++) {
            var expressionObject = resolveObjectExpression(arr[a].trim(), context);
            expressionObject.object.computedProperties.push({ computed: computed, element: element, attribute: attribute, expression: expression });
            computed.properties.push(expressionObject);
            rebindComputedProperty({ computed: computed, element: element, attribute: attribute, expression: expression });
        }
    };

    var rebindComputedProperty = function (object, currentElement) {
        // add binding functions for each dependency object
        for (var a = 0; a < object.computed.properties.length; a++) {
            (function (obj, element, attribute, computed, expression) {
                var f = null;
                if (attribute.indexOf('style.') === 0) {
                    var subAttribute = attribute.replace('style.', '');

                    f = function () {
                        // do not apply the computed property when it is not on
                        if (!computed.on) return;
                        var value = resolveFormatExpressionProperty(computed.f.call(computed.context), expression);

                        if (!value)
                            value = '';

                        element.style[subAttribute] = value;
                    };
                }
                else {
                    f = function () {
                        // do not apply the computed property when it is not on
                        if (!computed.on) return;
                        var value = resolveFormatExpressionProperty(computed.f.call(computed.context), expression);

                        if (!value)
                            value = '';

                        element[attribute] = value
                    };
                }
                if (Lava.typeOf(obj.object) === 'array') {
                    for (var b = 0; b < obj.object.length; b++) {
                        addBinding(obj.object[b].bindings[obj.property], f, attribute, element);
                    }
                }
                else {
                    addBinding(obj.object.bindings[obj.property], f, attribute, element);
                }
                computed.trigger = f;
                f();
            })(object.computed.properties[a], object.element, object.attribute, object.computed, object.expression);
        }
    };

    var addBinding = function (existingBindings, func, attribute, element) {
        // make sure that this binding does not already exist
        var exists = false;
        for (var c = 0; c < existingBindings.length; c++) {
            if (existingBindings[c].attribute === attribute && element == existingBindings[c].element) {
                exists = true;
                break;
            }
        }
        if (!exists) {
            existingBindings.push({ func: func, attribute: attribute, element: element });
        }
    };

    var resolveFormatExpressionProperty = function (value, expression) {
        if (expression.format)
            value = expression.format.replace('{0}', value);
        else if (expression.formatExclusive && value != null && value.toString().length > 0)
            value = expression.formatExclusive.replace('{0}', value);

        if (value === null) {
            value = '';
        }

        return value;
    };

    // Adds the template html to the element
    // tagName - the name of the tag of the element
    // html - the html to add to the element
    // element - the element to add the html to
    // index - the child index to add the html at. -1 = end
    // append - true to append html to the element, false to overwrite it
    // before - specifies an element to add the html before
    var addTemplate = function (tagName, html, element, index, append, before) {
        // If the tag is one of these types, we need to parse the resulting html string 
        // and manually append the table rows and columns.
        if (tagName === 'table' || tagName === 'tbody') {
            html = html.replace(/\<\/tr\>/gi, '').replace(/\<\/td\>/gi, '');
            var rows = html.split('<tr>');

            for (var a = 0; a < rows.length; a++) {
                // create a new row
                var row = element.insertRow(index);
                var cells = rows[a].replace(/\n/g, '').split('<td>');

                var hasContent = false;
                for (var b = 0; b < cells.length; b++) {
                    if (cells[b].trim().length > 0) {
                        // create a new cell 
                        var cell = row.insertCell(-1);
                        cell.innerHTML = cells[b];
                        hasContent = true;
                    }
                }

                if (!hasContent)
                    element.deleteRow(index);
            }
        }
        else if (tagName === 'thead') {
            html = html.replace(/\<\/tr\>/gi, '').replace(/\<\/th\>/gi, '');
            var rows = html.split('<tr>');

            for (var a = 0; a < rows.length; a++) {
                // create a new row
                var row = element.insertRow(index);
                var cells = rows[a].replace(/\n/g, '').split('<th>');

                var hasContent = false;
                for (var b = 0; b < cells.length; b++) {
                    if (cells[b].trim().length > 0) {
                        // create a new cell 
                        var cell = row.insertCell(-1);
                        cell.innerHTML = cells[b];
                        hasContent = true;
                    }
                }

                if (!hasContent)
                    element.deleteRow(index);
            }
        }
        else if (tagName === 'tr') {
            html = html.replace(/\<\/td\>/gi, '');
            var cells = html.split('<td>');

            for (var b = 0; b < cells.length; b++) {
                // create a new cell 
                var cell = element.insertCell(-1);
                cell.innerHTML = cells[b];
            }
        }
        else {
            if (index === -1 && !append) {
                element.innerHTML = html;
            }
            else if (index === -1 && append) {
                element.insertAdjacentHTML('beforeend', html);
            }
            else {
                var tempElement = document.createElement('div');
                tempElement.innerHTML = html;
                element.insertBefore(getFirstChild(tempElement), element.children[index]);
                delete tempElement;
            }
        }
    }

    Lava.Object = function (a) {
        var a_copy = {};
        Lava.extend(a_copy, a);
        addGetSet(a);
        a.clone = function (b) {
            var cloned = b || {};
            Lava.extend(cloned, a_copy);
            addGetSet(cloned);
            // apply proper context to all computed properties
            for (var prop in a) {
                if (a.hasOwnProperty(prop)) {
                    var property = a[prop];
                    if (Lava.typeOf(property) === 'computed') {
                        cloned[prop] = {
                            type: 'computed',
                            dependencies: property.dependencies,
                            f: property.f,
                            on: true,
                            context: cloned
                        };

                        (function (obj) {
                            obj.get = function () {
                                return obj.f.call(obj.context);
                            };
                        }(cloned[prop]));
                    }
                }
            }
            cloned.sync = function (c) {
                Lava.sync(cloned, c);
                return cloned;
            }
            return cloned;
        };
        a.extend = function (b) {
            Lava.extend(a, b);
            Lava.extend(a_copy, b);
            addGetSet(a);
        };
        a.sync = function (b) {
            Lava.sync(a, b);
            return a;
        };
        return a;
    };

    // takes in a normal javascript array and returns an array consisting of Lava.Object
    Lava.Array = function (array, object) {
        if (Lava.typeOf(array) == 'array') {
            var lavaArray = [];
            var arrayLen = array.length;

            for (var i = 0; i < arrayLen; i++) {
                var o = array[i];
                if (o.bindings) {
                    // object is already a Lava.Object
                    lavaArray.push(o);
                }
                else {
                    var type = Lava.typeOf(o);

                    if (type === 'array') {
                        // o is an array so we need to call createArray recursively
                        lavaArray.push(Lava.Array(o));
                    }
                    else if (type === 'object') {
                        // o is already an object so we just need to create a Lava.Object from it
                        if (object) {
                            lavaArray.push(object.clone(o));
                        }
                        else {
                            lavaArray.push(Lava.Object(o));
                        }
                    }
                    else {
                        // lava is not an array or object so we need to treat it as a single property
                        lavaArray.push(Lava.Object({ value: o }));
                    }
                }
            }

            return lavaArray;
        }
        return null;
    }

    Lava.Controller = function (a) {
        addGetSet(a);
        a.extend = function (b) {
            Lava.extend(a, b);
            addGetSet(a);
        };
        return a;
    };
    /// COMPUTED PROPERTY
    // Sets up a computed property that depends on other properties in the root object or a child object
    // dependencies: optional parameter for describing properties that this computed property should depend on.
    // It can have special syntax but in simple case its comma delimited list of property names that this computed property 
    // should depend on.  Every time one of these dependent properties changes, a change request will be sent to the computed 
    // property as well.
    // f: the function that makes up the computed property
    Lava.Computed = function (dependencies, f, context) {
        var hasDependencies = typeof dependencies === "string",
            args = Array.prototype.slice.call(arguments);
        if (!hasDependencies) {
            args.unshift(null);
        }

        var computed = {
            type: 'computed',
            dependencies: args[0],
            f: args[1],
            on: true,
            context: args[2]
        };

        computed.get = function () {
            return computed.f.call(computed.context);
        };

        return computed;
    };

    /// END COMPUTED PROPERTY

    Lava.ready(function () {
        // create attribute maps
        if (document.innerText)
            attributeMaps.text = 'innerText';
        else
            attributeMaps.text = 'textContent';

        attributeMaps.html = 'innerHTML';
        attributeMaps.class = "className";
        attributeMaps.show = 'if';
        attributeMaps.hide = 'ifnot';

        Lava.bind();
    });

    window.Lava = Lava;
})(window);